50. 众筹项目 v2

创建注册中心

主程序开启注册中心功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.itguigu.zcw;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@EnableEurekaServer // 开启注册中心功能
@SpringBootApplication
public class ZcwRegisterApplication {

public static void main(String[] args) {
SpringApplication.run(ZcwRegisterApplication.class, args);
}
}

添加 application.yml 配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
spring:
application:
name: ZCW-REGISTER
server:
port: 8761

eureka:
instance:
hostname: localhost
client:
register-with-eureka: false #自己就是注册中心,不用注册自己
fetch-registry: false #要不要去注册中心获取其他服务的地址
service-url:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

创建用户模块

创建一个 Spring Starter Project,zcw-user。功能主要是注册,登录,会员中心,密码找回等。需要加上 web, MySQL ,JDBC, MyBatis, Eureka Discovery, Redis 等相关模块。

创建项目模块

创建一个 Spring Starter Project,zcw-project。功能主要是众筹项目的发布,创建等。需要加上 web, MySQL ,JDBC, MyBatis, Eureka Discovery, Redis 等相关模块。

步骤和创建用户模块差不多,图略

创建订单模块

创建一个 Spring Starter Project,zcw-order。功能和支付相关。需要加上 web, MySQL ,JDBC, MyBatis, Eureka Discovery, Redis 等相关模块。

步骤和创建用户模块差不多,图略

创建公共模块

创建一个 Maven 工程 (jar),zcw-commons(以前已经创建 zcw-common 项目了,加个 s )。不继承父工程,因为父项目要依赖于当前项目,否则出现循环依赖。

创建父工程

创建一个 Maven 工程 (pom),zcw-parents(以前已经创建 zcw-parent 项目了,加个 s ),父工程继承 SpringBoot 父工程,其他自定义项目继承 zcw-parents 项目,并且聚合其他自定义项目,父工程依赖于 zcw-commons 工程, 注意不是依赖管理。其他项目就不用再配置对zcw-commons 项目的依赖了。

父工程继承 SpringBoot 父工程

1
2
3
4
5
6
7
 <!-- 父工程继承 SpringBoot 父工程 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.6.RELEASE</version>
<relativePath/>
</parent>

修改每个子工程的配置文件,让子工程继承父工程

1
2
3
4
5
6
7
<!-- 子工程继承父工程 -->
<parent>
<groupId>com.atguigu.zcw</groupId>
<artifactId>zcw-parents</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>../zcw-parents/pom.xml</relativePath>
</parent>

父工程聚合子工程,并且依赖 commons 工程

集成 Druid

在父工程中增加对 druid 数据源依赖

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.12</version>
</dependency>

在 user 模块中配置 application.yml 配置文件,配置数据库连接信息,eurake 等信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
spring:
application:
name: SCW-USER

datasource:
username: root
password: 123456
url: jdbc:mysql://127.0.0.1:3306/zcw?useSSL=false&useUnicode=true&characterEncoding=UTF-8
driver-class-name: com.mysql.jdbc.Driver

mybatis:
config-location: classpath:/mybatis/mybatis-config.xml
mapper-locations: classpath:/mybatis/mapper/*.xml

eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
instance:
appname: SCW-USER
prefer-ip-address: true

server:
port: 7000

配置 Druid 数据源配置类,一方面是让其读取 yml 中的配置文件,创建 DataSource 实例。一方面是配置 Druid 监控。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package com.itguigu.zcw.user.config;

import java.sql.SQLException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

import javax.sql.DataSource;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.support.http.StatViewServlet;
import com.alibaba.druid.support.http.WebStatFilter;

@Configuration
public class AppDruidConfig {
@ConfigurationProperties(prefix = "spring.datasource")
@Bean
public DataSource dataSource() throws SQLException {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setFilters("stat"); // 设置监控 SQL 的过滤器
return dataSource;
}

@Bean
public ServletRegistrationBean statViewServlet() {
ServletRegistrationBean bean = new ServletRegistrationBean(new StatViewServlet(), "/druid/*");
Map<String, String> initParams = new HashMap<>();
initParams.put("loginUsername", "admin");
initParams.put("loginPassword", "123456");
initParams.put("allow", "");// 默认就是允许所有访问
// initParams.put("deny", "192.168.15.21");
bean.setInitParameters(initParams);
return bean;
}

@Bean
public FilterRegistrationBean webStatFilter() {
FilterRegistrationBean bean = new FilterRegistrationBean();
bean.setFilter(new WebStatFilter());
Map<String, String> initParams = new HashMap<>();
initParams.put("exclusions", "*.js,*.css,/druid/*");
bean.setInitParameters(initParams);
bean.setUrlPatterns(Arrays.asList("/*"));
return bean;
}
}

在 resource 资源文件夹下新建 mybatis 文件夹, mybatis 文件夹下新建 mapper 文件夹, mybatis 文件夹下创建 mybatis-config.xml 文件, 内容如下

1
2
3
4
5
6
7
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

</configuration>

主程序上加上包扫描和开启服务注册发现和事务等功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.itguigu.zcw;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@EnableTransactionManagement // 开启事务管理
@MapperScan("com.itguigu.zcw.user.mapper")
@EnableDiscoveryClient // 开启服务注册发现服务
@SpringBootApplication
public class ZcwUserApplication {
public static void main(String[] args) {
SpringApplication.run(ZcwUserApplication.class, args);
}
}

测试数据源信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.itguigu.zcw;

import java.sql.Connection;
import java.sql.SQLException;

import javax.sql.DataSource;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class ZcwUserApplicationTests {
@Autowired
DataSource dataSource;

@Test
void testDatasource() throws SQLException {
Connection connection = dataSource.getConnection();
System.out.println(connection);
// com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@421ead7e
connection.close(); // 放回链接池
}
}

集成 slf4j+logback

SpringBoot 底层默认使用的就是 slf4j+logback 日志框架,所以我们只需要添加 logback.xml 文件即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%-4relative [%thread] %-5level %logger{35} - %msg %n</pattern>
</encoder>
</appender>

<root level="DEBUG">
<appender-ref ref="STDOUT" />
</root>

</configuration>

集成 Redis

通过以下两个类可以查看 redis-starter 的相关信息
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguratio
org.springframework.boot.autoconfigure.data.redis.RedisProperties

创建项目的时候已经引入了 redis-starter,所以不必再次添加,直接在 yml 中配置 redis 地址即可。RedisTemplate(可以操作对象)、StringRedisTemplate(建议用这个, 将对象整成json字符串对象)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
spring:
application:
name: SCW-USER

datasource:
username: root
password: 123456
url: jdbc:mysql://127.0.0.1:3306/zcw?useSSL=false&useUnicode=true&characterEncoding=UTF-8
driver-class-name: com.mysql.jdbc.Driver
redis: # 配置 Redis
host: 127.0.0.1
port: 6379

mybatis:
config-location: classpath:/mybatis/mybatis-config.xml
mapper-locations: classpath:/mybatis/mapper/*.xml

eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
instance:
appname: SCW-USER
prefer-ip-address: true

server:
port: 7000

测试 redis 使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
void testRedis() {
// 设置值
stringRedisTemplate.opsForValue().set("test", "test value");
// 获取值
String string = stringRedisTemplate.opsForValue().get("test");
System.out.println(string); // test value

// 设置值
redisTemplate.opsForValue().set("zhangsan", "zhangsan");
// 获取值
Object object = redisTemplate.opsForValue().get("zhangsan");
System.out.println(object); // zhangsan
}

集成 Swagger

在父工程中添加依赖

1
2
3
4
5
6
7
8
9
10
11
<!-- swagger -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>

在 user 模块下的 com.itguigu.zcw.user.config 包中编写配置类,开启 swagger2 自动生成 api 文档的功能

1
2
3
4
5
6
7
8
9
10
11
package com.itguigu.zcw.user.config;

import org.springframework.context.annotation.Configuration;

import springfox.documentation.swagger2.annotations.EnableSwagger2;

@Configuration // 声明这是一个配置类
@EnableSwagger2 // 开启 swagger2 自动生成 api 文档的功能
public class AppSwaggerConfig {

}

启动项目,访问地址 http://127.0.0.1:7000/swagger-ui.html 就能看到 swagger 的页面了

导入已经设计好的接口文档信息

接口文档和 Controller 已经在另一个项目中设计好了,直接导入

集成 lombok

lombok 外号叫做小辣椒, 因为 logo 是小辣椒的样子。https://www.projectlombok.org/

common 项目中引入 lombok:

1
2
3
4
5
6
<!-- 引入 lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.8</version>
</dependency>

复制 maven 仓库中的projectlombok/lombok/1.18.12/ 下的 lombok-1.18.12.jar包到 STS 的安装目录中,并去掉 jar 包的版本号,且在 STS.ini 文件中最后一行加上一下内容,配置方法参考 此处

1
2
-javaagent:../Eclipse/lombok.jar
-vmargs -javaagent:lombok.jar

重启 STS,导入对应包即可。lombok 常见注解如下:
@Data:提供getter/setter
@NoArgsConstructor: 无参构造器 @RequiredArgsConstructor @AllArgsConstructor 全参数构造器
@EqualsAndHashCode:提供equals和hashCode方法
@Log:快速的使用slf4j日志
@Log4j:快速使用log4j日志
@Log4j2:快速使用log4j2
@Getter/@Setter
@Slf4j 内置log对象,直接调用日志方法输出日志
@ToString

HttpClient

HttpClient 版本已经停止使用,被 HttpComponents 取代。HttpUtils 工具类代码参考地址依赖。在 commons 模块中,com.itguigu.zcw.http 包下创建 HttpUtils 类,将 github上中的代码拷贝其中,并在 pom 文件中引入依赖。

接口统一返回

在 commons 模块中,com.itguigu.zcw.vo.resp 包下创建 AppResponse。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.itguigu.zcw.vo.resp;

import com.itguigu.zcw.enums.ResponseCodeEnume;

import lombok.Data;

/**
* 应用统一返回结果数据封装类
* @author Administrator
* @param <T> 返回结果数据类型
*/
@Data
public class AppResponse<T> {

private Integer code;
private String msg;
private T data;


/**
* 快速响应成功
* @param data
* @return
*/
public static<T> AppResponse<T> ok(T data){
AppResponse<T> resp = new AppResponse<>();
resp.setCode(ResponseCodeEnume.SUCCESS.getCode());
resp.setMsg(ResponseCodeEnume.SUCCESS.getMsg());
resp.setData(data);
return resp;
}

/**
* 快速响应失败
*/
public static<T> AppResponse<T> fail(T data){
AppResponse<T> resp = new AppResponse<>();
resp.setCode(ResponseCodeEnume.FAIL.getCode());
resp.setMsg(ResponseCodeEnume.FAIL.getMsg());
resp.setData(data);
return resp;
}
}

封装短信接口

采用阿里云第三方短信接口 进行短信的发送,封装如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package com.itguigu.zcw.user.components;

import java.util.HashMap;
import java.util.Map;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.util.EntityUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import com.itguigu.zcw.http.HttpUtils;
import com.itguigu.zcw.vo.resp.AppResponse;

import lombok.extern.slf4j.Slf4j;

/**
* 发送手机短信验证码的模板类
* @author Administrator
*
*/
@Slf4j
@Component
public class SmsTemplate {
@Value("${sms.host}")
String host ;

@Value("${sms.path}")
String path ;

@Value("${sms.method}")
String method ;

@Value("${sms.appcode}")
String appcode ;

public AppResponse<String> sendCode(Map<String, String> querys) {

log.debug("开始发送短信-参数:{}", querys);

Map<String, String> headers = new HashMap<String, String>();
// 最后在header中的格式(中间是英文空格)为Authorization:APPCODE 83359fd73fe94948385f570e3c139105
headers.put("Authorization", "APPCODE " + appcode);

Map<String, String> bodys = new HashMap<String, String>();

try {

HttpResponse response = HttpUtils.doPost(host, path, method, headers, querys, bodys);
System.out.println(response.toString());
// 获取response的body
HttpEntity entity = response.getEntity();
System.out.println(EntityUtils.toString(response.getEntity()));

log.debug("开始发送短信-成功:{},{}", querys.get("mobile"),querys.get("param"));

return AppResponse.ok("OK");
} catch (Exception e) {
log.debug("开始发送短信-失败:{}", e.getMessage());
return AppResponse.fail("fail");
}
}
}

使用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 @ApiOperation(value = "发送短信验证码")
@PostMapping("/sendsms")
public AppResponse<Object> sendsms(String number) {
StringBuffer code = new StringBuffer();
Random random = new Random();
for (int i = 0; i < 4; i++) {
code.append(random.nextInt(10));
}
HashMap<String, String> querys = new HashMap<>();
querys.put("mobile", number);
querys.put("param", "code:" + code.toString());
querys.put("tpl_id", "TP1711063");

AppResponse<String> status = smsTemplate.sendCode(querys);
if (0 == status.getCode()) {
// 存入 redis
stringRedisTemplate.opsForValue().set(number, code.toString());
log.info("发送短信成功!");
return AppResponse.ok(status.getMsg());
}else {
log.info("发送短信失败!");
return AppResponse.fail(status.getMsg());
}
}

逆向工程

数据库表设计好之后,可以使用 mbg 插件逆向生成 Vo。在父工程 pom 中引入 mbg 插件(这样工程也能使用)

1
2
3
4
5
6
<!-- mbg -->
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>${mbg.version}</version>
</dependency>

引入 xml 文件,并配置好数据库账号密码,要逆向的表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">

<generatorConfiguration>

<context id="MySQLTables" targetRuntime="MyBatis3">

<!-- 不生成 -->
<commentGenerator>
<property name="suppressAllComments" value="true"/>
</commentGenerator>


<!-- mvn mybatis-generator:generate 配置数据库位置 ,配置虚拟机上的mysql ip地址;不采用安全协议连接,否则无法逆向生成 -->
<jdbcConnection driverClass="com.mysql.jdbc.Driver"
connectionURL="jdbc:mysql://127.0.0.1:3306/zcw?useSSL=false"
userId="root" password="123456">
</jdbcConnection>

<!-- 数据库中的字段为 int,float 类型是否强制转换为 BigDecimal 类型 -->
<javaTypeResolver>
<property name="forceBigDecimals" value="false" />
</javaTypeResolver>


<!-- javaBean生成在哪里 -->
<javaModelGenerator
targetPackage="com.itguigu.zcw.user.bean"
targetProject="../zcw-user/src/main/java">
<property name="enableSubPackages" value="true" />
<property name="trimStrings" value="true" />
</javaModelGenerator>

<!-- sqlMap sql映射文件(xml mapper文件) -->
<sqlMapGenerator targetPackage="mybatis.mapper"
targetProject="../zcw-user/src/main/resources">
<property name="enableSubPackages" value="true" />
</sqlMapGenerator>

<!-- javaClient:java接口生成的地方 -->
<javaClientGenerator type="XMLMAPPER"
targetPackage="com.itguigu.zcw.user.mapper"
targetProject="../zcw-user/src/main/java">
<property name="enableSubPackages" value="true" />
</javaClientGenerator>

<!-- 设置需要生成的表 (这里是全部)-->
<!-- <table schema="" tableName="%"></table> -->

<!-- 设置需要生成的表 (这里是单独指定)-->
<table tableName="t_member"></table>
<table tableName="t_member_address"></table>
<table tableName="t_member_cert"></table>
<table tableName="t_member_project_follow"></table>
<table tableName="t_message"></table>
</context>
</generatorConfiguration>

在父工程中(这样工程也能使用) pom 中配置和 Maven 做集成的插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- mbg 逆向插件, 运行  mvn mybatis-generator:generate 就能逆向生成 bean 和抽象 mapper 信息 -->
<build>
<plugins>
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.3.7</version>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.42</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>

执行 Maven 命令 mybatis-generator:generate 生成相关信息

第一次运行出现以下错误信息

1
[ERROR] Failed to execute goal on project zcw-user: Could not resolve dependencies for project com.itguigu.zcw:zcw-user:jar:0.0.1-SNAPSHOT: Could not find artifact com.atguigu.zcw:zcw-commons:jar:0.0.1-SNAPSHOT -> [Help 1]

原因是 user 继承类父模块,父模块依赖了 commons 模块,我们只需执行 Maven 命令 install 一下 commons 模块即可。然后重新运行 mybatis-generator:generate 就能生成相关的 mapper 和实体信息。

注册

新建 com.itguigu.zcw.user.vo.req 包,用于存放注册请求参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.itguigu.zcw.user.vo.req;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.ToString;

@ApiModel // swagger
@Data // lombok
@ToString
public class UserRegistVo {
@ApiModelProperty("手机号")
private String loginacct;

@ApiModelProperty("密码")
private String userpaswd;

@ApiModelProperty("邮箱")
private String email;

@ApiModelProperty("验证码")
private String code;

@ApiModelProperty("用户类型 0-个人,1-企业")
private String usertype;
}

controller 方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
@ApiOperation(value = "用户注册")
@PostMapping("/register")
public AppResponse<Object> register(UserRegistVo userRegistVo) {
log.info("用户注册,接收到请求参数:{}", userRegistVo);

// 获取注册手机号码
String loginacct = userRegistVo.getLoginacct();
if (!StringUtils.isEmpty(loginacct)) {
// 校验验证码
String code = stringRedisTemplate.opsForValue().get(loginacct);
if (!StringUtils.isEmpty(code) && code.equals(userRegistVo.getCode())) {
// 校验邮箱格式是否正确
boolean isEmail = ValidationEmail.isEmail(userRegistVo.getEmail());
if(!isEmail) {
AppResponse<Object> resp = AppResponse.fail(null);
resp.setMsg("邮箱格式不正确");
return resp;
}
try {
// 校验邮箱是否已经存在
Boolean emailExist = userService.emailExist(userRegistVo.getEmail());
if (emailExist) {
AppResponse<Object> resp = AppResponse.fail(null);
resp.setMsg("邮箱已存在");
return resp;
}
// 校验账户是否已经存在
Boolean loginExist = userService.loginacctExist(userRegistVo.getUserpaswd());
if (emailExist) {
AppResponse<Object> resp = AppResponse.fail(null);
resp.setMsg("账号已被注册");
return resp;
}

// 注册
int i = userService.saveMember(userRegistVo);
if (i==1) {
// 删除验证码信息
stringRedisTemplate.delete(loginacct);
AppResponse<Object> resp = AppResponse.ok("ok");
resp.setMsg("注册成功");
return resp;
}else {
AppResponse<Object> resp = AppResponse.fail(null);
resp.setMsg("注册失败");
return resp;
}
} catch (Exception e) {
log.error("注册失败, 系统错误:{}", e.getMessage());
AppResponse<Object> resp = AppResponse.fail(null);
resp.setMsg("系统错误");
return resp;
}
}else {
AppResponse<Object> resp = AppResponse.fail(null);
resp.setMsg("验证码已经过期, 请重新获取");
return resp;
}
}else {
AppResponse<Object> resp = AppResponse.fail(null);
resp.setMsg("请输入注册手机号");
return resp;
}
}

UserService.impi 实现如下,里面的 BeanUtils.copyProperties 是将 Vo 转换为 bean 实体对象的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
package com.itguigu.zcw.user.service.impl;

import java.util.List;

import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import com.itguigu.zcw.user.bean.TMember;
import com.itguigu.zcw.user.bean.TMemberExample;
import com.itguigu.zcw.user.bean.TMemberExample.Criteria;
import com.itguigu.zcw.user.enums.UserExceptionEnum;
import com.itguigu.zcw.user.exception.UserException;
import com.itguigu.zcw.user.mapper.TMemberMapper;
import com.itguigu.zcw.user.service.UserService;
import com.itguigu.zcw.user.vo.req.UserRegistVo;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
public class UserServiceImpl implements UserService {
@Autowired
TMemberMapper memberMapper;

@Override
public Boolean emailExist(String email) {
try {
TMemberExample tMemberExample = new TMemberExample();
Criteria tmemCriteria = tMemberExample.createCriteria();
tmemCriteria.andEmailEqualTo(email);
List<TMember> memberList = memberMapper.selectByExample(tMemberExample);
// 判断邮箱是否存在
return memberList.size()>0 ? true : false;
} catch (Exception e) {
throw new UserException(UserExceptionEnum.EMAIL_EXIST);
}

}

@Override
public int saveMember(UserRegistVo userRegistVo) {
try {
// 将 Vo 转换为 TMember 实体对象 (名称匹配的就转, 不匹配就不转,但是也不报错)
TMember tMember = new TMember();
BeanUtils.copyProperties(userRegistVo, tMember);
// 设置其他前段没有传递,但是数据库不能为空的参数
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String bCryptPassword = bCryptPasswordEncoder.encode(userRegistVo.getUserpaswd());
// userpswd
tMember.setUserpswd(bCryptPassword);
// 设置 username
tMember.setUsername(userRegistVo.getLoginacct());
// 新增操作
return memberMapper.insertSelective(tMember);
} catch (Exception e) {
log.error("新增用户失败:{}", e.getMessage());
throw new UserException(UserExceptionEnum.REGIST_ERROR);
}
}

@Override
public Boolean loginacctExist(String userpaswd) {
try {
TMemberExample tMemberExample = new TMemberExample();
Criteria tmemCriteria = tMemberExample.createCriteria();
tmemCriteria.andUserpswdEqualTo(userpaswd);
List<TMember> memberList = memberMapper.selectByExample(tMemberExample);
// 判断邮箱是否存在
return memberList.size()>0 ? true : false;
} catch (Exception e) {
throw new UserException(UserExceptionEnum.LOGINACCT_EXIST);
}
}
}

这里面还涉及自定义错误和错误枚举,处理接口统一返回等问题,值得看看。

文件上传 OSS

在 zcw-project 模块下参考阿里云 OSS SDK 编写AliOssTemplate 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package com.itguigu.zcw.components;

import java.io.InputStream;
import java.util.UUID;

import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;

import lombok.Data;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@ToString
@Data
public class AliOssTemplate {
// 这里不使用 @Value 注解读取 properties 中的配置,而是使用配置的方式,详见 com.itguigu.zcw.config 下 AliOssConfig.java
// 注意加上 @Data 注解,否则值不能注入进来
// 上传地址
private String endpoint;
// accessKeyId
private String accessKeyId;
// accessKeySecret
private String accessKeySecret;
// bucketName
private String bucketName;
// 上传存储文件夹
private String uploadFolderName;


public String upload(InputStream inputStream, String uploadFileName) {
log.info("endpoint:{}", endpoint);
log.info("accessKeyId:{}", accessKeyId);
log.info("accessKeySecret:{}", accessKeySecret);
log.info("bucketName:{}", bucketName);
log.info("uploadFolderName:{}", uploadFolderName);
log.info("uploadFileName:{}", uploadFileName);

String fileName;

try {
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
// 上传内容到指定的存储空间(bucketName)并保存为指定的文件名称(objectName)。
String fileSuffix = UUID.randomUUID().toString().replace("-", "");
fileName = fileSuffix + "_" + uploadFileName;
ossClient.putObject(bucketName, uploadFolderName + "/" + fileName, inputStream);
// 关闭OSSClient。
ossClient.shutdown();
} catch (Exception e) {
return "";
}
// 返回上传后文件完整路径
// https://osssssssss.oss-cn-shanghai.aliyuncs.com/zcw/2020-04-11_17-28-06.png
return "https://" + bucketName + ".oss-cn-shanghai.aliyuncs.com/" + uploadFolderName + "/" + fileName;
}
}

这里的配置采用配置类的方式注入,不是之前的 @Value, 配置类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.itguigu.zcw.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.itguigu.zcw.components.AliOssTemplate;

// 使用 @Configuration 或者 @SpringBootConfiguration 让其成为一个配置类
@Configuration
public class AliOssConfig {

// 从 Properties 中读取前缀为 oss 的配置值,并且注入到 AliOssTemplate 属性中去
@ConfigurationProperties(prefix = "oss")
@Bean
public AliOssTemplate aliOssTemplate() {
return new AliOssTemplate();
}
}

controller 代码如下,这里可以接受上传的多个文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@ApiOperation(value = "上传图片")
@PostMapping("/upload")
public AppResponse<Object> upload(@RequestParam("files") MultipartFile[] files) {
ArrayList<String> fileUrlList = new ArrayList<>();
for (MultipartFile file : files) {
try {
String fileUrl = aliOssTemplate.upload(file.getInputStream(), file.getOriginalFilename());
fileUrlList.add(fileUrl);
} catch (IOException e) {
log.error("文件上传错误:{}", file.getOriginalFilename());
e.printStackTrace();
AppResponse<Object> resp = AppResponse.fail("");
resp.setMsg("文件上传失败");
}
}
return AppResponse.ok(fileUrlList);
}

项目创建

创建众筹的项目步骤是一步一步的,数据是在最后一步才提交的,所以我们要临时存储中间每一步的数据。所以每一个步骤的提交,参数都由一个小 Vo 进行收集,然后将小 Vo 整合到一个大 Vo 当中去,提交的最后一步得到一个信息完整的大 Vo,最后将大 Vo 转换成一个个需要存储的实体类进行保存,且这些保存都是在一个事务里面的。这里将收集每一个 Vo 的信息放入 Redis 进行临时存储,以便于传到下一个步骤,具体的需要用到 fastjson 的序列化和反序列操作(将对象转换为字符串,将字符串转换为对象)。fastjson 应用 string字符串转换成java对象或者对象数组

支付宝支付

支付宝支付需要用到 RSA2 密钥,需要将自己的公钥上传支付宝,私钥留着对参数进行加密。还要获取支付宝的公钥,对支付宝返回来的数据进行验签,防止数据在我发送给支付宝或者支付宝返回给我的时候遭到篡改。

SpringSession

解决 session 不一致有很多方案,但多配置复杂或者有明显的缺点。有了 SpringSession,所有的 session 由SpringSession 创建维护,无需我们修改任何代码,就能在集群环境下使用原生的 session 方式编程,无侵入、简单配置和 Spring 应用无缝整合、对接各种 session 存储方案。

新建项目,并且引入以下依赖:

application.properties 中进行配置:

1
2
3
4
5
6
7
8
9
10
11
# redis配置
spring.redis.host=127.0.0.1
spring.redis.port=6379

spring.redis.jedis.pool.max-idle=100

# springsession配置
spring.session.store-type=redis

# session过期时间
spring.session.timeout=1800

编写 controller 代码实现 session 的插入和读取。注意⚠️ 这里引入 spring session 之后,原来的 HttpSession 就被重写了,重写后操作的不在是以前的 session 域。(写法还和以前一样,但是操作的不是 session 域了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.itguigu.zcw.controller;

import javax.servlet.http.HttpSession;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class SpringSessionController {
@GetMapping("/get")
public String get(HttpSession session) {
// 使用 spring session 后 HttpSession 不再是以前的 HttpSession,而是被重写了
String attributeStr = (String) session.getAttribute("test");
System.out.println(session.getClass());
// org.springframework.session.web.http.SessionRepositoryFilter$SessionRepositoryRequestWrapper$HttpSessionWrapper
return attributeStr;
}

@GetMapping("/set")
public String set(HttpSession session) {
// 使用 spring session 后 HttpSession 不再是以前的 HttpSession,而是被重写了
session.setAttribute("test", "test spring session");
System.out.println(session.getClass());
// org.springframework.session.web.http.SessionRepositoryFilter$SessionRepositoryRequestWrapper$HttpSessionWrapper
return "ok";
}
}

访问 http://localhost:8080/get,http://localhost:8080/set 进行测试。

Thymeleaf

官网地址:https://www.thymeleaf.org/
搭建 web 项目。使用 Thymeleaf 和前端页面进行整合,并使用 Feign 调用后面的微服务。

demo

继承父工程

1
2
3
4
5
6
7
<!-- 子工程继承父工程 -->
<parent>
<groupId>com.atguigu.zcw</groupId>
<artifactId>zcw-parents</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>../zcw-parents/pom.xml</relativePath>
</parent>

配置文件中进行相应配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
spring:
application:
name: SCW-WEB

# thymeleaf 配置
thymeleaf:
prefix: classpath:/templates/
suffix: .html
cache: false #开发的时候禁用缓存

# spring session 设置
session:
# spring session 使用 redis
store-type: redis
# spring session 过期时间
timeout: 1800

# 配置 Redis
redis:
host: 127.0.0.1
port: 6379

# redis 连接数设置
jedis:
pool:
max-idle: 100


eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
instance:
appname: SCW-WEB
prefer-ip-address: true

server:
port: 7005

feign:
hystrix:
enabled: true

创建 Controller 和 前端页面,并在页面中取值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.itguigu.zcw.web.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class DispatcherController {
@RequestMapping("/index")
public String index(Model model) {
model.addAttribute("key", "value");
return "index";
}
}

注意: 在页面中需要引入 xml 的命名空间,值是 http://www.thymeleaf.org

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>thymeleaf 测试</title>
</head>
<body>
<div th:text="${key}">
div 内容
</div>
</body>
</html>

访问地址 http://localhost:7005/index 就能看到返回的内容。

常用语法

th:text

1
<div th:text="${key}">div 内容</div>

th:each

1
<div th:each="user: ${users}" th:text="${user}"></div>
1
<div th:each="user: ${users}">[[${user}]]</div>

加载静态文件,下面例子中的 href 和 src 都是原生的属性,th:href 和 th:src 的值会覆盖原生属性的值,@{/} 这种写法自动的项目根路径下加载文件。因为 th:href 和 th:src 会覆盖原有的 href 和 src 属性,所以这里不要原有的 href 和 src 属性也行,如果不要就意味着只能以这种模版渲染的方式运行,直接用浏览器打开这个静态的页面是没有效果的。所以最好还是两种方式都使用。

1
<link href="/static/bootstrap/css/bootstrap.css" th:href="@{/static/bootstrap/css/bootstrap.css}"/>
1
<script type="text/javascript" src="/static/static/bootstrap/js/bootstrap.min.js" th:src="@{/static/bootstrap/js/bootstrap.min.js}"></script>

其他常见语法课参考此处

WebMvcConfigurer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.itguigu.zcw.web.config;

import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

public class AppWebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 添加拦截器
}

@Override
public void addViewControllers(ViewControllerRegistry registry) {
// 如果 controller 中路由只是起到跳转的作用,那么我们可以直接写一个映射
// 下面写法的意思是当访问 /index 的时候,直接跳转到 resource/template/index 页面。
// 这种用法不多,当作了解即可
registry.addViewController("/index").setViewName("index");
}
}

还可以配置资源过滤,跨域等信息。

多环境配置

只需要在创建多个 yml 配置文件,然后在 properties 中指定启用的哪个环境就行。例如存在以下配置文件

1
2
3
4
5
drwxr-xr-x  5 rex  staff  160  5  1 15:16 ./
drwxr-xr-x 5 rex staff 160 5 1 15:18 ../
-rw-r--r-- 1 rex staff 376 4 10 16:12 application-dev.yml
-rw-r--r-- 1 rex staff 376 5 1 15:17 application-prod.yml
-rw-r--r-- 1 rex staff 47 5 1 15:19 application.properties

只需要在 application.properties 中指定使用哪一个配置文件即可

1
2
# 指定启用环境
spring.profiles.active=dev

项目打包部署

可以直接用 STS 的 Maven 插件,直接输入 package,或者直接在命令行运行 mvn package 运行即可。在对其他项目打包之前,需要把 common 项目先安装到仓库中,因为其他项目依赖 common 项目。

安装依赖项目

1
mvn clean install -Dmaven.test.skip=true

打包

1
mvn package

或者跳过运行测试用例进行打包

1
mvn package -Dmaven.test.skip=true

maven 各命令的区别和声明周期:

  1. package命令完成了项目编译、单元测试、打包功能,但没有把打好的可执行jar包(war包或其它形式的包)布署到本地maven仓库和远程maven私服仓库
  2. install命令完成了项目编译、单元测试、打包功能,同时把打好的可执行jar包(war包或其它形式的包)布署到本地maven仓库,但没有布署到远程maven私服仓库
  3. deploy命令完成了项目编译、单元测试、打包功能,同时把打好的可执行jar包(war包或其它形式的包)布署到本地maven仓库和远程maven私服仓库

所以在进行项目打包的时候:

  1. 先对 common 项目进行打包,并安装到本地 maven 仓库,这样其他工程才能找到

    1
    mvn install -Dmaven.test.skip=true  # install 就做了 package 的工作,所以不用单独执行 install
  2. 修改各个工程的配置文件

  3. 对订单服务进行打包

    1
    mvn package -Dmaven.test.skip=true
  4. 对注册中心进行打包

    1
    mvn package -Dmaven.test.skip=true
  5. 对用户模块进行打包

    1
    mvn package -Dmaven.test.skip=true
  6. 对 web 模块进行打包

    1
    mvn package -Dmaven.test.skip=true
  7. 对项目模块进行打包

1
mvn package -Dmaven.test.skip=true
  1. 父工程不用打包

  2. 最后将打好的五个包分别从 target 中拷贝出来

启动项目

1
2
3
4
5
nohup java -jar zcw-register-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod > ./zcw-register.log &
nohup java -jar zcw-order-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod > ./zcw-order.log &
nohup java -jar zcw-project-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod > ./zcw-project.log &
nohup java -jar zcw-user-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod > ./zcw-user.log &
nohup java -jar zcw-web-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod > ./zcw-web.log &